// Copyright (c) 2011 Blue Onion Software, All rights reserved
using System;
using System.Globalization;
using System.IO;
using System.Web.UI;
using System.Xml;

namespace BlueOnionSoftware.Bloget
{
    internal static class XmlRpc
    {
        internal static XmlRpcMethodCall ReadMethod(XmlReader reader)
        {
            Throw.IfNull(reader, "reader");

            var methodCall = new XmlRpcMethodCall();
            reader.ReadStartElement("methodCall");
            methodCall.Name = reader.ReadElementString("methodName");

            if (reader.IsStartElement("params"))
            {
                reader.ReadStartElement("params");

                do
                {
                    methodCall.AddParam(ReadParam(reader));
                } while (reader.IsStartElement("param"));
            }

            reader.ReadEndElement();
            reader.ReadEndElement();
            return methodCall;
        }

        internal static XmlRpcValue ReadParam(XmlReader reader)
        {
            Throw.IfNull(reader, "reader");

            reader.ReadStartElement("param");
            var value = ReadValue(reader);
            reader.ReadEndElement();
            return value;
        }

        private static XmlRpcValue ReadValue(XmlReader reader)
        {
            Throw.IfNull(reader, "reader");

            XmlRpcValue value;
            reader.ReadStartElement("value");

            if (reader.IsStartElement())
            {
                switch (reader.Name)
                {
                    case "i4":
                    case "int":
                        value = new XmlRpcValue(reader.ReadElementContentAsInt());
                        break;

                    case "boolean":
                        // Some implementations ignore the specification and use true/false instead of 1/0
                        var booleanString = reader.ReadElementString();
                        int boolean;

                        if (int.TryParse(booleanString, out boolean) == false)
                        {
                            booleanString = booleanString.Trim();

                            if (string.Compare(booleanString, "false", StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                boolean = 0;
                            }

                            else if (string.Compare(booleanString, "true", StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                boolean = 1;
                            }

                            else
                            {
                                boolean = 2;
                            }
                        }

                        if (boolean != 0 && boolean != 1)
                        {
                            throw new InvalidDataException("boolean value not zero or one in ReadValue(reader)");
                        }

                        value = new XmlRpcValue(boolean == 1);
                        break;

                    case "string":
                        value = new XmlRpcValue(reader.ReadElementString());
                        break;

                    case "double":
                        value = new XmlRpcValue(reader.ReadElementContentAsDouble());
                        break;

                    case "dateTime.iso8601":
                        // XML-RPC is not iso8601 which is a real drag.
                        var dts = reader.ReadElementString();
                        var year = int.Parse(dts.Substring(0, 4), CultureInfo.InvariantCulture);
                        var month = int.Parse(dts.Substring(4, 2), CultureInfo.InvariantCulture);
                        var day = int.Parse(dts.Substring(6, 2), CultureInfo.InvariantCulture);
                        var hour = int.Parse(dts.Substring(9, 2), CultureInfo.InvariantCulture);
                        var min = int.Parse(dts.Substring(12, 2), CultureInfo.InvariantCulture);
                        var second = int.Parse(dts.Substring(15, 2), CultureInfo.InvariantCulture);
                        var date = new DateTime(year, month, day, hour, min, second, DateTimeKind.Utc);
                        value = new XmlRpcValue(date);
                        break;

                    case "base64":
                        var buffer = new byte[8192];

                        using (var stream = new MemoryStream())
                        {
                            int bytesRead;
                            while ((bytesRead = reader.ReadElementContentAsBase64(buffer, 0, buffer.Length)) > 0)
                            {
                                stream.Write(buffer, 0, bytesRead);
                            }

                            stream.Flush();
                            value = new XmlRpcValue(stream.ToArray());
                        }
                        break;

                    case "struct":
                        value = new XmlRpcValue(ReadStruct(reader));
                        break;

                    case "array":
                        value = new XmlRpcValue(ReadArray(reader));
                        break;

                    default:
                        throw new InvalidDataException("Unrecognized value type in ReadValue(reader)");
                }
            }

            else
            {
                value = new XmlRpcValue(reader.ReadContentAsString());
            }

            reader.ReadEndElement();
            return value;
        }

        internal static XmlRpcStruct ReadStruct(XmlReader reader)
        {
            Throw.IfNull(reader, "reader");

            var xmlRpcStruct = new XmlRpcStruct();
            reader.ReadStartElement("struct");

            while (reader.IsStartElement("member"))
            {
                var member = ReadMember(reader);
                xmlRpcStruct.Add(member);
            }

            // Empty element test
            if (xmlRpcStruct.Members.Length > 0)
            {
                reader.ReadEndElement();
            }

            return xmlRpcStruct;
        }

        internal static XmlRpcMember ReadMember(XmlReader reader)
        {
            Throw.IfNull(reader, "reader");

            var member = new XmlRpcMember();
            reader.ReadStartElement("member");
            member.Name = reader.ReadElementString("name");
            member.Value = ReadValue(reader);
            reader.ReadEndElement();
            return member;
        }

        internal static XmlRpcArray ReadArray(XmlReader reader)
        {
            Throw.IfNull(reader, "reader");

            var xmlRpcArray = new XmlRpcArray();
            reader.ReadStartElement("array");
            reader.ReadStartElement("data");

            while (reader.IsStartElement("value"))
            {
                var value = ReadValue(reader);
                xmlRpcArray.Add(value);
            }

            // Empty element test
            if (xmlRpcArray.Values.Length > 0)
            {
                reader.ReadEndElement();
            }

            reader.ReadEndElement();
            return xmlRpcArray;
        }

        /// <summary>
        /// Helper method to create response stream formatted for text/xml, utf-8
        /// </summary>
        /// <param name="page"></param>
        /// <returns></returns>
        internal static XmlWriter CreateXmlWriter(Page page)
        {
            page.Response.ClearHeaders();
            page.Response.ClearContent();
            page.Response.ContentType = "text/xml";
            page.Response.Charset = "utf-8";

            var xmlSettings = new XmlWriterSettings();
            xmlSettings.Indent = true;

            return XmlWriter.Create(page.Response.OutputStream, xmlSettings);
        }
    }
}